/**
 * @file WinSerialReader.cc
 * @author Bono.ASUN.TamGitsun (gitsun.tam@outlook.com)
 * @brief 
 * @version 0.1
 * @date 2022-02-06
 * 
 * @copyright Copyright (c) 2022
 * 
 */


#include "WinSerialReader.hh"
#include <utils/asserts.hh>
#include <utils/time.hh>
#include <utils/logger/Logger.hh>
#include <sstream>
#include <cstring>


using namespace TGS::utils;


#define LOG_TAG "WinSerialReader"
#define THIS_FILE "WinSerialReader.cc"


namespace TGS::readers {


const std::string WinSerialReader::PROPERTY_DEVICE = "Device";
const std::string WinSerialReader::PROPERTY_BAUD_RATE = "BaudRate";
const std::string WinSerialReader::PROPERTY_BYTE_SIZE = "ByteSize";
const std::string WinSerialReader::PROPERTY_PARITY = "Parity";
const std::string WinSerialReader::PROPERTY_STOP_BITS = "StopBits";


void WinSerialReader::set_property(const std::string & key, const std::string & value)
{
    if (PROPERTY_DEVICE == key)
    {
        ASSERT_RETURN_VOID(value.length() > 3 && 0 == strncmp(value.c_str(), "COM", 3));
        std::stringstream ss;
        size_t index = 3;
        int num = std::stoi(value.substr(3));
        if (num >= 10)
        {
            ss << "\\\\.\\" << value << ":";
        } else
        {
            ss << value << ":";
        }
        Reader::set_property(key, ss.str());
    }
    Reader::set_property(key, value);
}

bool WinSerialReader::open(void)
{
    do {
        std::string device = get_property(PROPERTY_DEVICE, "");
        ASSERT_BREAK(device.length() > 0);
        mlog_i(LOG_TAG, THIS_FILE, "Device: %s", device.c_str());
        std::string baud_rate_str = get_property(PROPERTY_BAUD_RATE, "");
        ASSERT_BREAK(baud_rate_str.length() > 0);
        int baud_rate = std::stoi(baud_rate_str);
        mlog_i(LOG_TAG, THIS_FILE, "BaudRate: %d", baud_rate);
        std::string byte_size_str = get_property(PROPERTY_BYTE_SIZE, "8");
        int byte_size = std::stoi(byte_size_str);
        mlog_i(LOG_TAG, THIS_FILE, "Byte Size: %d", byte_size);
        const std::string parity_str = get_property(PROPERTY_PARITY, "N");
        mlog_i(LOG_TAG, THIS_FILE, "Parity: %s", parity_str.c_str());
        int parity = NOPARITY;
        if (parity_str.length() > 0)
        {
            switch (parity_str[0])
            {
            case 'O':
                parity = ODDPARITY;
                break;
            case 'E':
                parity = EVENPARITY;
                break;
            case 'M':
                parity = MARKPARITY;
                break;
            case 'S':
                parity = SPACEPARITY;
                break;
            case 'N':
            default:
                parity = NOPARITY;
                break;
            }
        }
        std::string stop_bits_str = get_property(PROPERTY_STOP_BITS, "1");
        float stop_bits_f = std::stof(stop_bits_str);
        int stop_bits;
        if (2 == stop_bits_f)
        {
            stop_bits = TWOSTOPBITS;
        } else if (1.5 == stop_bits_f)
        {
            stop_bits = ONE5STOPBITS;
        } else
        {
            stop_bits = ONESTOPBIT;
        }
        mlog_i(LOG_TAG, THIS_FILE, "Stop Bits: %d", stop_bits);

        // Create Handle
        mCommHandle = CreateFileA(device.c_str(), GENERIC_READ|GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
        ASSERT_BREAK(INVALID_HANDLE_VALUE != mCommHandle);

        // Get the configures of Serial
        DCB dcb;
        ASSERT_BREAK(GetCommState(mCommHandle, &dcb));

        // Configure the Serial
        dcb.BaudRate = baud_rate;  // BaudRate
        dcb.fBinary = TRUE;  // Binary mode, must be TRUE
        dcb.ByteSize = byte_size;  // Byte size, 4~8
        dcb.StopBits = stop_bits;  // Stop bits
        dcb.fParity = NOPARITY == parity ? FALSE : TRUE;  // Odd-Even parity
        dcb.Parity = parity;  // Parity mode
        dcb.fOutxCtsFlow = FALSE;  // CTS handshake
        dcb.fOutxDsrFlow = FALSE;  // DST handshake
        dcb.fDtrControl = DTR_CONTROL_ENABLE;  // DTR control
        dcb.fDsrSensitivity = FALSE;
        dcb.fTXContinueOnXoff = FALSE;
        dcb.fOutX = FALSE;  // Use XON/XOFF protocol or not
        dcb.fInX = FALSE;  // Use XON/XOFF protocol or not
        dcb.fErrorChar = FALSE;  // Use send error protocol or not
        dcb.fNull = FALSE;  // NULL stripping or not
        dcb.fRtsControl = RTS_CONTROL_ENABLE;
        dcb.fAbortOnError = FALSE;  // Abort or not while error happens

        // Set the configures of Serial
        ASSERT_BREAK(SetCommState(mCommHandle, &dcb));

        // Set the timeout of read/write
        COMMTIMEOUTS timeouts;
        GetCommTimeouts(mCommHandle, &timeouts);
        timeouts.ReadIntervalTimeout = MAXDWORD;
        timeouts.ReadTotalTimeoutMultiplier = 0;
        timeouts.ReadTotalTimeoutConstant = 0;
        timeouts.WriteTotalTimeoutMultiplier = 10;
        timeouts.WriteTotalTimeoutConstant = 1000;
        ASSERT_BREAK(SetCommTimeouts(mCommHandle, &timeouts));

        // Create a thread to read Serial's data
        mRun = true;
        mThread = std::thread(WinSerialReader::read_thread, this);

        return true;
    } while (0);
    // Do some cleanning
    if (INVALID_HANDLE_VALUE != mCommHandle)
    {
        CloseHandle(mCommHandle);
        mCommHandle = INVALID_HANDLE_VALUE;
    }
    return false;
}

bool WinSerialReader::close(void)
{
    mRun = false;
    if (mThread.joinable())
    {
        mThread.join();
    }
    if (INVALID_HANDLE_VALUE != mCommHandle)
    {
        CloseHandle(mCommHandle);
        mCommHandle = INVALID_HANDLE_VALUE;
    }
    return true;
}

bool WinSerialReader::reset(void)
{
    close();
    {
        std::lock_guard<std::mutex> l(mReadMutex);
        mHasReadData = false;
        mReadPtrR = mReadPtrW = mReadData;
    }
    return open();
}

int32_t WinSerialReader::send(uint8_t * data, int32_t size)
{
    ASSERT_RETURN(INVALID_HANDLE_VALUE != mCommHandle && data, -1);
    // Clear the Serial
    PurgeComm(mCommHandle, PURGE_RXCLEAR|PURGE_TXCLEAR);
    // Write the Serial
    DWORD n_writes = 0;
    DWORD ret = WriteFile(mCommHandle, data, static_cast<DWORD>(size), &n_writes, nullptr);
    // Clear the Serial
    PurgeComm(mCommHandle, PURGE_RXCLEAR|PURGE_TXCLEAR);
    return ret ? n_writes : -1;
}

int32_t WinSerialReader::read(uint8_t * buffer, int32_t size)
{
    ASSERT_RETURN(buffer, -1);
    std::lock_guard<std::mutex> l(mReadMutex);
    int32_t availables = available_fl();
    int32_t read_size = size > availables ? availables : size;
    if (mReadPtrW >= mReadPtrR)
    {
        memcpy(buffer, mReadPtrR, read_size);
        mReadPtrR += read_size;
    } else
    {
        size_t offset = sizeof(mReadData) - (mReadPtrR - mReadData);
        memcpy(buffer, mReadPtrR, offset);
        memcpy(buffer + offset, mReadData, read_size - offset);
        mReadPtrR = mReadData + read_size - offset;
    }
    if (mReadPtrR == mReadPtrW)
    {
        mHasReadData = false;
    }
    return read_size;
}

int32_t WinSerialReader::available(void)
{
    std::lock_guard<std::mutex> l(mReadMutex);
    return available_fl();
}

WinSerialReader::~WinSerialReader(void)
{
    close();
}

void WinSerialReader::read_thread(WinSerialReader * reader)
{
    ASSERT_RETURN_VOID(INVALID_HANDLE_VALUE != reader->mCommHandle);
    // Clear the Serial
    PurgeComm(reader->mCommHandle, PURGE_RXCLEAR|PURGE_TXCLEAR);
    uint8_t read_data[sizeof(reader->mReadData)];
    uint8_t * ptr_w = read_data + (reader->mReadPtrW - reader->mReadData);
    while (reader->mRun)
    {
        DWORD n_reads = 0;
        uint8_t * ptr_r;
        {
            std::lock_guard<std::mutex> l(reader->mReadMutex);
            ptr_r = read_data + (reader->mReadPtrR - reader->mReadData);
        }
        if (ptr_w > ptr_r  // The write position is behind the read position
            || !reader->mHasReadData && ptr_r == ptr_w)  // No data read
        {
            DWORD size = static_cast<DWORD>(sizeof(read_data) - (ptr_w - read_data));
            BOOL rc = ReadFile(reader->mCommHandle, ptr_w, size, &n_reads, nullptr);
            if (rc && n_reads >= size)
            {
                mlog_v(LOG_TAG, THIS_FILE, "read %d bytes", n_reads);
                {
                    std::lock_guard<std::mutex> l(reader->mReadMutex);
                    memcpy(reader->mReadPtrW, ptr_w, n_reads);
                    reader->mReadPtrW = reader->mReadData;
                    reader->mHasReadData = true;
                }
                ptr_w = read_data;
            } else if (n_reads > 0)
            {
                mlog_v(LOG_TAG, THIS_FILE, "read %d bytes", n_reads);
                {
                    std::lock_guard<std::mutex> l(reader->mReadMutex);
                    memcpy(reader->mReadPtrW, ptr_w, n_reads);
                    reader->mReadPtrW += n_reads;
                    reader->mHasReadData = true;
                }
                ptr_w += n_reads;
            }
        }
        if (ptr_r > ptr_w)  // The write position is before the read position
        {
            DWORD size = static_cast<DWORD>(ptr_r - ptr_w);
            BOOL rc = ReadFile(reader->mCommHandle, ptr_w, size, &n_reads, nullptr);
            if (rc && n_reads > 0)
            {
                mlog_v(LOG_TAG, THIS_FILE, "read %d bytes", n_reads);
                {
                    std::lock_guard<std::mutex> l(reader->mReadMutex);
                    memcpy(reader->mReadPtrW, ptr_w, n_reads);
                    reader->mReadPtrW += n_reads;
                }
                ptr_w += n_reads;
            }
        }
        msleep(10);
    }
}

int32_t WinSerialReader::available_fl(void)
{
    if (mReadPtrR > mReadPtrW)
    {
        return static_cast<int32_t>(sizeof(mReadData) + mReadPtrW - mReadPtrR);
    }
    return static_cast<int32_t>(mReadPtrW - mReadPtrR);
}


}
